<template>
  <scroll-view :style="{height}" class="infinite-list-container" scroll-y @scroll="scrollEvent">
    <div class="infinite-list-phantom" :style="phantomStyle"></div>
    <div class="infinite-list" :style="contentStyle">
      <div class="infinite-list-item" :id="item._index" :key="item._index" v-for="item in visibleData">
        <slot :item="item"></slot>
      </div>
    </div>
  </scroll-view>
</template>

<script>

export default {
  name: 'VirtualList',
  props: {
    // 所有列表数据
    list: {
      type: Array,
      default: () => [],
    },
    // 预估高度
    estimatedItemSize: {
      type: Number,
      required: true,
    },
    // 缓冲区比例
    bufferScale: {
      type: Number,
      default: 1,
    },
    // 容器高度 100px or 50vh
    height: {
      type: String,
      default: '100%',
    },
  },
  data () {
    return {
      // 可视区域高度
      screenHeight: 0,
      // 起始索引
      start: 0,
      // 结束索引
      end: 0,

      phantomHeight: 0,
      startOffset: 0,
    }
  },
  computed: {
    _list () {
      return this.list.map((item, index) => {
        return {
          _index: `_${index}`,
          ...item,
        }
      })
    },
    visibleCount () {
      return Math.ceil(this.screenHeight / this.estimatedItemSize)
    },
    aboveCount () {
      return Math.min(this.start, this.bufferScale * this.visibleCount)
    },
    belowCount () {
      return Math.min(this.list.length - this.end, this.bufferScale * this.visibleCount)
    },
    visibleData () {
      const start = this.start - this.aboveCount
      const end = this.end + this.belowCount
      return this._list.slice(start, end)
    },

    phantomStyle () {
      return `height: ${this.phantomHeight}px;`
    },
    contentStyle () {
      return `transform: translate3d(0, ${this.startOffset}px, 0)`
    },
  },
  created () {
    this.initPositions()
  },
  mounted () {
    this.screenHeight = this.$el.clientHeight
    this.start = 0
    this.end = this.start + this.visibleCount
  },
  updated () {
    console.log('updated')
    this.$nextTick(function () {
      // if (!this.$refs.items || !this.$refs.items.length) {
      //   return
      // }
      // 获取真实元素大小，修改对应的尺寸缓存
      this.updateItemsSize()
      // 更新列表总高度
      const height = this.positions[this.positions.length - 1].bottom
      this.phantomHeight = height
      // 更新真实偏移量
      this.setStartOffset()
    })
  },

  methods: {
    initPositions () {
      this.positions = this.list.map((d, index) => ({
        index,
        height: this.estimatedItemSize,
        top: index * this.estimatedItemSize,
        bottom: (index + 1) * this.estimatedItemSize,
      }))
    },

    // 获取列表项的当前尺寸
    updateItemsSize () {
      const query = uni.createSelectorQuery().in(this)
      query.selectAll('.infinite-list-item').boundingClientRect(res => {
        res.forEach(node => {
          const { height } = node
          const index = +node.id.slice(1)
          const oldHeight = this.positions[index].height
          const dValue = oldHeight - height
          // 存在差值
          if (dValue) {
            this.positions[index].bottom = this.positions[index].bottom - dValue
            this.positions[index].height = height
            for (let k = index + 1; k < this.positions.length; k++) {
              this.positions[k].top = this.positions[k - 1].bottom
              this.positions[k].bottom = this.positions[k].bottom - dValue
            }
          }
        })
      }).exec()
    },
    // 获取当前的偏移量
    setStartOffset () {
      let startOffset
      if (this.start >= 1) {
        const size = this.positions[this.start].top - (this.positions[this.start - this.aboveCount] ? this.positions[this.start - this.aboveCount].top : 0)
        startOffset = this.positions[this.start - 1].bottom - size
      } else {
        startOffset = 0
      }
      this.startOffset = startOffset
    },
    // 滚动事件
    scrollEvent (e) {
      // 当前滚动位置
      const scrollTop = e.detail.scrollTop
      // 此时的开始索引
      this.start = this.getStartIndex(scrollTop)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
    },

    // 获取列表起始索引
    getStartIndex (scrollTop = 0) {
      // 二分法查找
      return this.binarySearch(this.positions, scrollTop)
    },
    binarySearch (list, value) {
      let start = 0
      let end = list.length - 1
      let tempIndex = null

      while (start <= end) {
        const midIndex = parseInt((start + end) / 2)
        const midValue = list[midIndex].bottom
        if (midValue === value) {
          return midIndex + 1
        } else if (midValue < value) {
          start = midIndex + 1
        } else if (midValue > value) {
          if (tempIndex === null || tempIndex > midIndex) {
            tempIndex = midIndex
          }
          end = end - 1
        }
      }
      return tempIndex
    },
  },
}
</script>

<style scoped>
.infinite-list-container {
  overflow: auto;
  position: relative;
  width: 100vw !important;
  height: 100vh !important;
  -webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}

.infinite-list-item {
  padding: 5px;
  color: #555;
  box-sizing: border-box;
  border-bottom: 1px solid #999;
  /* height:200px; */
}

</style>
